package git.soulbgm.job;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import git.soulbgm.common.enums.Status;
import git.soulbgm.common.okhttp.OkHttpUtil;
import git.soulbgm.config.SysConfig;
import git.soulbgm.pojo.*;
import git.soulbgm.service.*;
import git.soulbgm.utils.JsonTool;
import git.soulbgm.utils.LogHelper;
import git.soulbgm.utils.StringUtil;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import static git.soulbgm.common.MonitorConstants.*;

/**
 * 服务心跳监测任务
 *
 * @author SoulBGM
 * @date 2021/01/29
 */
@Service
public class ServiceHeartbeatMonitorTask {

    private static final LogHelper LOG = new LogHelper(ServiceHeartbeatMonitorTask.class);

    private final ServiceInfoService serviceInfoService;
    private final ServiceStatusService serviceStatusService;
    private final ServiceStartingService serviceStartingService;
    private final StartupService startupService;
    private final AlarmInfoReport alarmInfoReport;
    private final Integer currentRegionCode;

    public ServiceHeartbeatMonitorTask(ServiceInfoService serviceInfoService, ServiceStatusService serviceStatusService, ServiceStartingService serviceStartingService, StartupService startupService, AlarmInfoReport alarmInfoReport, SysConfig config) {
        this.serviceInfoService = serviceInfoService;
        this.serviceStatusService = serviceStatusService;
        this.serviceStartingService = serviceStartingService;
        this.startupService = startupService;
        this.alarmInfoReport = alarmInfoReport;
        this.currentRegionCode = config.getRegionCode();
    }

    @Scheduled(cron = "${jobs.map.serviceHeartbeatMonitoring.cron}")
    @SchedulerLock(name = "${jobs.map.serviceHeartbeatMonitoring.name}", lockAtMostFor = "${jobs.map.serviceHeartbeatMonitoring.lockAtMostFor}", lockAtLeastFor = "${jobs.map.serviceHeartbeatMonitoring.lockAtLeastFor}")
    public void execute() throws Exception {
        Date collectTime = new Date();
        Map<Long, ServiceInfo> map = serviceInfoService.getCacheServiceInfoAll().stream()
                .filter(o -> Status.NORMAL.code.equals(o.getStatus()) && MONITOR_MODE_PASSIVITY == o.getMonitorMode()).collect(Collectors.toMap(ServiceInfo::getId, Function.identity()));
        if (map.isEmpty()) {
            return;
        }
        List<ServiceStatus> statusList = map.values().stream()
                .map(o -> getServiceStatus(o, collectTime)).collect(Collectors.toList());
        int size = map.size();
        CountDownLatch latch = new CountDownLatch(size);
        for (ServiceStatus serviceStatus : statusList) {
            ServiceInfo info = map.get(serviceStatus.getId());
            String url = StringUtil.format("http://{}:{}/{}", info.getDeployIp(), info.getPort(), info.getHealthCheckUrl());
            OkHttpUtil.get(url, new ServiceHeartbeatMonitorCallback(serviceStatus, latch));
        }

        boolean isExecuteComplete = latch.await(10L * size, TimeUnit.SECONDS);
        if (!isExecuteComplete) {
            LOG.error("执行服务心跳监测时间超时");
        }

        for (ServiceStatus serviceStatus : statusList) {
            ServiceInfo info = map.get(serviceStatus.getId());
            serviceStatus.setStatus(checkStartup(serviceStatus, info));
            recordAlarm(serviceStatus);
        }

        serviceStatusService.reset(statusList);
    }

    /**
     * 获得服务状态
     *
     * @param info        信息
     * @param collectTime 收集时间
     * @return {@link ServiceStatus}
     */
    private ServiceStatus getServiceStatus(ServiceInfo info, Date collectTime) {
        ServiceStatus serviceStatus = new ServiceStatus();
        serviceStatus.setId(info.getId());
        serviceStatus.setServiceCode(info.getServiceCode());
        serviceStatus.setNodeCode(info.getNodeCode());
        serviceStatus.setServiceName(info.getServiceName());
        serviceStatus.setServiceChineseName(info.getServiceChineseName());
        serviceStatus.setDeployIp(info.getDeployIp());
        serviceStatus.setServiceStartupTime(0L);
        serviceStatus.setDetectionTime(collectTime);
        serviceStatus.setRegionCode(currentRegionCode);
        serviceStatus.setStatus(Status.NOT_INITIATED.code);
        return serviceStatus;
    }

    /**
     * 检查启动
     *
     * @param serviceStatus 节点状态
     * @param info          节点信息
     */
    private Integer checkStartup(ServiceStatus serviceStatus, ServiceInfo info) {
        Integer status = serviceStatus.getStatus();
        Integer selfStart = info.getSelfStart();
        Integer estimatedStartupTime = info.getEstimatedStartupTime();

        ServiceStarting starting = new ServiceStarting(serviceStatus.getNodeCode(),
                serviceStatus.getServiceName() + "_" + serviceStatus.getServiceCode(),
                RULE_TYPE_SERVICE,
                serviceStatus.getDeployIp());
        if (Status.NOT_INITIATED.code.equals(status)) {
            // 查看 启动表中是否有记录
            ServiceStarting serviceStarting = serviceStartingService.getOne(new QueryWrapper<>(starting));
            // 如果没有则添加
            if (serviceStarting == null && Status.YES.code.equals(selfStart)) {
                starting.setStartupTime(new Date());
                serviceStartingService.save(starting);
                startupService.startupService(serviceStatus.getNodeCode(), serviceStatus.getServiceCode());
                status = Status.STARTING.code;
            } else if (serviceStarting != null) {
                // 如果有则判断是否超过预计时间
                Date startUpTime = serviceStarting.getStartupTime();
                Date nowDate = new Date();
                long difference = nowDate.getTime() - startUpTime.getTime();
                if ((difference / ONE_SECONDS_MILLISECOND) >= estimatedStartupTime) {
                    // 如果超过则设置状态为 未启动
                    status = Status.NOT_INITIATED.code;
                } else {
                    // 如果没有超过则设置状态为 启动中
                    status = Status.STARTING.code;
                }
            } else {
                // 不需要自动启动则设置状态为 未启动
                status = Status.NOT_INITIATED.code;
            }
        } else if (Status.NORMAL.code.equals(status)) {
            // 删除 启动表中的记录
            serviceStartingService.remove(new QueryWrapper<>(starting));
        }
        return status;
    }

    /**
     * 记录报警
     *
     * @param serviceStatus 服务状态
     */
    public void recordAlarm(ServiceStatus serviceStatus) {
        Integer status = serviceStatus.getStatus();
        if (Status.STARTING.code.equals(status)) {
            // 服务重启
            String alarmContent = StringUtil.format("{} 服务在IP为 {}的服务器节点宕机，现在正在尝试重启", serviceStatus.getServiceName(), serviceStatus.getDeployIp());
            LOG.warn(alarmContent);
            alarmInfoReport.serviceRebootAlarm(serviceStatus.getNodeCode(), serviceStatus.getServiceCode(), alarmContent);
        } else if (Status.NOT_INITIATED.code.equals(status)) {
            // 服务宕机
            String alarmContent = StringUtil.format("{} 服务在IP为 {}的服务器节点，尝试重启没有成功现在处于宕机", serviceStatus.getServiceName(), serviceStatus.getDeployIp());
            LOG.warn(alarmContent);
            alarmInfoReport.serviceShutdownAlarm(serviceStatus.getNodeCode(), serviceStatus.getServiceCode(), alarmContent);
        } else if (Status.NORMAL.code.equals(status)) {
            // 服务正常
            alarmInfoReport.serviceStartingClearAlarm(serviceStatus.getNodeCode(), serviceStatus.getServiceCode());
        }
    }

}

/**
 * 服务心跳监测回调
 *
 * @author SoulBGM
 * @date 2023-01-06
 */
class ServiceHeartbeatMonitorCallback implements Callback {

    private static final String UP = "up";

    private final ServiceStatus serviceStatus;
    private final CountDownLatch latch;

    public ServiceHeartbeatMonitorCallback(ServiceStatus nodeStatus, CountDownLatch latch) {
        this.serviceStatus = nodeStatus;
        this.latch = latch;
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        ResponseBody body = response.body();
        if (body != null) {
            ServiceRunningStatus serviceRunningStatus = JsonTool.parse(body.string(), ServiceRunningStatus.class);
            if (serviceRunningStatus != null) {
                serviceStatus.setServiceStartupTime(serviceRunningStatus.getStartupTime());
                serviceStatus.setStatus(UP.equals(serviceRunningStatus.getStatus()) ? Status.NORMAL.code : Status.NOT_INITIATED.code);
            }
        }
        latch.countDown();
    }

    @Override
    public void onFailure(Call call, IOException e) {
        latch.countDown();
    }
}
